import {
  Component,
  OnInit,
  Input,
  Output,
  Inject,
  Renderer2,
  forwardRef,
  ViewChild,
  EventEmitter,
  ElementRef,
  ViewContainerRef,
  TemplateRef,
  ChangeDetectionStrategy,
  ViewEncapsulation,
  ChangeDetectorRef,
  ContentChild,
  OnDestroy,
} from '@angular/core';
import { Router } from '@angular/router';
import { AnimationEvent } from '@angular/animations';
import { TemplatePortal } from '@angular/cdk/portal';
import { Overlay, OverlayConfig, PositionStrategy } from '@angular/cdk/overlay';
import { combineLatest, isObservable, merge, Observable, Subject } from 'rxjs';
import { auditTime, takeUntil, distinctUntilChanged, take } from 'rxjs/operators';
import { OverlayService, MenuBaseService, MenuService, LvSizeLD, showItem, showSubItem } from '../core/api';
import { routerNavigate, TypeUtils } from '../core/utils/api';
import { ExtendOverlayConfig } from '../core/overlay/api';
import { POSITION_MAP, ADJUST_POSITION_MAP, getPosition } from './dropdown-position';
import { MenuItem } from './interface';

@Component({
  selector: 'lv-dropdown',
  exportAs: 'lvDropdown',
  template: `
    <ng-template #dropdownTemplate let-data>
      <ul
        lv-submenu
        class="lv-dropdown-panel level0"
        [@showItem]="_animationState"
        (@showItem.start)="onAnimationStart($event)"
        (@showItem.done)="onAnimationDone($event)"
        (mouseenter)="handleMouseEnterOrLeave(true)"
        (mouseleave)="handleMouseEnterOrLeave(false)"
        [class]="lvPanelClass"
        [ngClass]="{ 'lv-theme-dark': lvTheme == 'dark', 'lv-dropdown-size-large': lvSize === 'large' }"
        [lvMenuList]="lvMenus"
        [lvItemClickClose]="lvItemClickClose"
        [lvCurrentHoverId]="hoverItemId"
        [lvMenuLevel]="0"
      ></ul>
    </ng-template>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  providers: [MenuService, MenuBaseService],
  host: {
    '[class.lv-dropdown-host]': 'true',
  },
  animations: [showItem],
})
export class DropdownComponent implements OnInit, OnDestroy {
  @Input() lvMenus: MenuItem[];

  @Input() lvPanelClass: string;

  @Input() lvDisabled: boolean = false;

  @Input() lvSize: LvSizeLD = 'default';

  @Input() lvItemClickClose: boolean = true;

  @Input() lvLabelTemplate: TemplateRef<void>;

  @Input() lvTheme: 'light' | 'dark' = 'light'; // 待实现

  @Output() lvShow: EventEmitter<any> = new EventEmitter();

  @Output() lvHide: EventEmitter<any> = new EventEmitter();

  @Output() lvItemClick: EventEmitter<any> = new EventEmitter();

  @Input() lvTrigger: 'click' | 'hover' | 'contextmenu' = 'click';

  // 点击overlay外部时是否关闭
  @Input() lvOuterClosable: boolean | { includes?: string[] } = true;

  @ViewChild('dropdownTemplate', { static: false })
  dropdownTemplate: TemplateRef<any>;

  @ContentChild('lvDropdownTrigger', { read: ElementRef, static: false })
  dropdownTrigger: ElementRef<any>;

  @Input() lvPosition: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' =
    'bottomLeft';

  @Input() lvBeforeOpen: () =>
    | (false | void | object)
    | Promise<false | void | object>
    | Observable<boolean | void | object>;

  public target;
  public _overlay;
  public mouseevent: MouseEvent;
  public _animationState = 'void';
  public dropopen$ = new Subject<boolean>();

  private _tempMenu;
  private _direction;
  private _hoverItemId;
  private _activeParentIds;
  private _animationState$;
  private _isOpening = false;
  private typeUtils = TypeUtils;
  private destroy$ = new Subject<void>();
  private openState$ = new Subject<boolean>();

  constructor(
    private el: ElementRef,
    private renderer: Renderer2,
    private cdr: ChangeDetectorRef,
    private viewContainerRef: ViewContainerRef,
    private overlay: Overlay,
    private overlayService: OverlayService,
  ) {}

  get hoverItemId(): any {
    return this._hoverItemId;
  }

  set hoverItemId(val: any) {
    this.reset();
    if (val) {
      this.getItemPath(val);
      this._hoverItemId = val;
    }
  }

  handleMouseEnterOrLeave(value): void {
    if (this.lvTrigger === 'hover') {
      this.dropopen$.next(value);
    }
  }

  closeDropDown(): void {
    setTimeout((): void => {
      if (this._overlay?.overlayRef.hasAttached()) {
        this._overlay.close();
      }
    }, 100);
  }

  onAnimationStart(event: AnimationEvent): void {
    if (event.toState === 'showUp' || event.toState === 'showDown') {
      this._isOpening = true;
    }
  }

  ngOnDestroy(): void {
    if (this._overlay?.overlayRef.hasAttached()) {
      this._overlay.overlayRef.dispose();
    }

    this.destroy$.next();
    this.destroy$.complete();
  }

  beforeDropdownClose(): void {
    this.openState$.next(true);
    this._animationState$.complete();

    if (this._direction === 'up') {
      this._animationState = 'hideUp';
    } else {
      this._animationState = 'hideDown';
    }
  }

  getItemPath(id): void {
    this._tempMenu = [];
    this._activeParentIds = [];

    this.flatMenus(this.lvMenus, null);
    this.getParentId(id);

    this._activeParentIds.forEach((item): void => {
      this.setHoverItem(item);
    });
  }

  reset(data?): void {
    let menus = data || this.lvMenus || [];
    menus.forEach((item): void => {
      if (item._parentHover) {
        item._parentHover = false;
      }

      if (item.items) {
        this.reset(item.items);
      }
    });
  }

  getParentId(id): void {
    const p = this._tempMenu.find((i): boolean => i.id === id && !this.typeUtils.isNotNil(i.parentId));

    if (p === undefined) {
      return;
    }

    this._activeParentIds.push(p.parentId);
    this.getParentId(p.parentId);
  }

  flatMenus(data, parentId): void {
    data.forEach((item): void => {
      const _item = {
        id: item.id,
        parentId,
        routerLink: item.routerLink || '',
      };
      this._tempMenu.push(_item);

      if (item.items && item.items.length !== 0) {
        this.flatMenus(item.items, item.id);
      }
    });
  }

  setHoverItem(value, data?): void {
    let menus = data || this.lvMenus || [];

    for (const item of menus) {
      if (!item || !item.id) {
        continue;
      }

      if (item.id === value) {
        item._parentHover = true;
        break;
      } else if (item?.items?.length) {
        this.setHoverItem(value, item.items);
      } else {
        continue;
      }
    }
  }

  onAnimationDone(event: AnimationEvent): void {
    const flag =
      (event.fromState === 'showUp' && event.toState === 'hideUp') ||
      (event.fromState === 'showDown' && event.toState === 'hideDown');
    if (flag) {
      this.openState$.next(false);
    }

    if (event.toState === 'showUp' || event.toState === 'showDown') {
      this._isOpening = false;
      this._animationState$.next(true);
    }
  }

  ngOnInit(): void {
    this.dropopen$.pipe(distinctUntilChanged(), auditTime(50), takeUntil(this.destroy$)).subscribe((open): void => {
      if (open) {
        this.openDropdown();
      } else {
        this.hoverItemId = null;
        this.closeDropDown();
      }
    });

    merge(this.dropopen$, this.openState$)
      .pipe(takeUntil(this.destroy$))
      .subscribe((): void => {
        this.cdr.markForCheck();
      });

    if (this.lvDisabled) {
      this.dropdownTrigger && (this.dropdownTrigger.nativeElement.style.color = '#999');
    }
  }

  beforeOpen(resolve): void {
    const openFn = (_resolve): void => {
      _resolve();
    };
    if (!this.lvBeforeOpen) {
      openFn(resolve);
    } else {
      const cb = this.lvBeforeOpen();
      if (cb instanceof Promise) {
        cb.then((res): void => {
          if (res !== false) {
            openFn(resolve);
          }
        });
      } else if (isObservable(cb)) {
        cb.subscribe((res): void => {
          if (res !== false) {
            openFn(resolve);
          }
        });
      } else if (cb !== false) {
        openFn(resolve);
      }
    }
  }

  getOverlayConfig(): OverlayConfig {
    let config = new OverlayConfig({
      hasBackdrop: false,
      minWidth: this.el.nativeElement.clientWidth,
      positionStrategy: this.overlay
        .position()
        .flexibleConnectedTo(this.target)
        .withPositions([POSITION_MAP[this.lvPosition], ...ADJUST_POSITION_MAP]),
      scrollStrategy: this.overlay.scrollStrategies.block(),
    });

    if (this.lvTrigger === 'contextmenu') {
      config = new OverlayConfig({
        hasBackdrop: false,
        positionStrategy: this.overlay
          .position()
          .global()
          .left(`${this.mouseevent.clientX + 1}px`)
          .top(`${this.mouseevent.clientY + 1}px`),
      });
    }
    return config;
  }

  updatePosition(event): void {
    const _disX =
      document.documentElement.clientWidth - event.clientX - this._overlay.overlayRef.overlayElement.clientWidth;
    const _disY =
      document.documentElement.clientHeight - event.clientY - this._overlay.overlayRef.overlayElement.clientHeight;
    const _top = _disY < 0 ? event.clientY - this._overlay.overlayRef.overlayElement.clientHeight : event.clientY;
    const _left = _disX < 0 ? event.clientX - this._overlay.overlayRef.overlayElement.clientWidth : event.clientX;

    if (_disX < 0 || _disY < 0) {
      const strategy: PositionStrategy = this.overlay.position().global().left(`${_left}px`).top(`${_top}px`);
      this._overlay.overlayRef.updatePositionStrategy(strategy);
    }
  }

  openDropdown(): void {
    if (this._overlay?.overlayRef.hasAttached()) {
      if (this.lvTrigger === 'hover') {
        if (this.el.nativeElement.style.opacity === 0) {
          this._overlay.overlayRef.dispose();
        } else {
          return;
        }
      }
      if (this.lvTrigger === 'click') {
        this._overlay.close();
        return;
      }
      if (this.lvTrigger === 'contextmenu') {
        this._overlay.overlayRef.dispose();
      }
    }

    const portal = new TemplatePortal(this.dropdownTemplate, this.viewContainerRef, {
      $implicit: this.lvMenus,
      level: 0,
    });

    const config = this.getOverlayConfig();
    const extendConfig = this.getExtendConfig();

    this._animationState$ = new Subject<boolean>();
    this.overlayService.open(portal, config, extendConfig);
  }

  getExtendConfig(): ExtendOverlayConfig {
    return {
      clickToCloseStrategy: this.lvOuterClosable === true ? { exclusions: '.lv-dropdown-item' } : this.lvOuterClosable,
      scrollToCloseStrategy: { exclusions: '.lv-dropdown-panel' },
      beforeOpen: (): Promise<any> =>
        new Promise((resolve): void => {
          this.beforeOpen(resolve);
        }),
      afterOpen: (overlayRef): void => {
        this._overlay = overlayRef;
        this.afterDropdownOpen(overlayRef);
      },
      beforeClose: (): Promise<void> =>
        new Promise<void>((resolve): void => {
          this.beforeDropdownClose();

          const closesubscribe = this.openState$.subscribe((open): void => {
            if (!open) {
              closesubscribe.unsubscribe();
              resolve();
            }
          });
        }),
      afterClose: (): void => {
        this.lvHide.emit();

        // 设置dropdown下拉图标方向
        this.dropdownTrigger && (this.dropdownTrigger.nativeElement.style.transform = 'scaleY(1)');

        this.dropopen$.next(false);
      },
    };
  }

  afterDropdownOpen(overlayInstance): void {
    // 设置z-index
    this.renderer.addClass(this._overlay.overlayRef.hostElement, 'lv-dropdown-z-index');

    // 打开后的回调函数
    this.lvShow.emit();

    // 设置dropdown下拉图标方向
    this.dropdownTrigger && (this.dropdownTrigger.nativeElement.style.transform = 'scaleY(-1)');

    // 如果是右键菜单，需要根据手动更新位置策略
    if (this.lvTrigger === 'contextmenu') {
      this.updatePosition(this.mouseevent);
    }

    if (this.lvTrigger === 'contextmenu') {
      this._animationState = 'showDown';
      this._direction = 'down';
      this.cdr.markForCheck();
    } else {
      // 获取打开的方向
      overlayInstance.overlayRef._positionStrategy.positionChanges.subscribe((e): void => {
        const _position = getPosition(e.connectionPair);
        if (_position === 'topLeft' || _position === 'topCenter' || _position === 'topRight') {
          this._animationState = 'showUp';
          this._direction = 'up';
        } else {
          this._animationState = 'showDown';
          this._direction = 'down';
        }
        this.cdr.markForCheck();
      });
    }
  }
}

@Component({
  selector: '[lv-submenu]',
  providers: [MenuBaseService, MenuService],
  templateUrl: './dropdown.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  host: {},
  animations: [showSubItem],
})
export class DropdownSubmenuComponent implements OnInit, OnDestroy {
  @Input() lvMenuLevel: any;

  @Input() lvMenuList: MenuItem[];

  @Input() lvCurrentHoverId: string;

  @Input() lvItemClickClose: boolean = true;

  @ViewChild('templatePortal', { static: false })
  menuPanelTemplate: TemplateRef<any>;

  public _dropdown;

  click$ = new Subject<any>();
  public typeUtils = TypeUtils;
  public _animationState = 'void';
  private _overlay;
  private destroy$ = new Subject<void>();
  private openState$ = new Subject<boolean>();

  constructor(
    private cdr: ChangeDetectorRef,
    public router: Router,
    private overlay: Overlay,
    private overlayService: OverlayService,
    private viewContainerRef: ViewContainerRef,
    private menuService: MenuService,
    private menuBaseService: MenuBaseService,
    @Inject(forwardRef((): typeof DropdownComponent => DropdownComponent)) dropdown: DropdownComponent,
  ) {
    this._dropdown = dropdown;
  }

  handleMouseEnterOrLeave(value): void {
    if (this._dropdown.lvTrigger === 'hover') {
      this._dropdown.dropopen$.next(value);
    }
  }

  onAnimationDone(event: AnimationEvent): void {
    if (event.fromState === 'show' && event.toState === 'hide') {
      this.openState$.next(false);
    }
  }

  ngOnDestroy(): void {
    if (this._overlay?.overlayRef.hasAttached()) {
      this._overlay.overlayRef.dispose();
    }

    this.destroy$.next();
    this.destroy$.complete();
  }

  setMouseEnterState(event, item, value: boolean): void {
    if (value && item) {
      let _target = null;
      let _item = null;
      if (item?.items) {
        _target = event.currentTarget;
        _item = item;
      }

      if (!item._parentHover) {
        this.openSubMenu(_target, _item);
      }
      this._dropdown.hoverItemId = item.id;
    }

    this.menuService.setMouseEnterState(value);
    if (this._dropdown.lvTrigger !== 'hover' && !value) {
      this._dropdown.hoverItemId = null;
    }
  }

  itemClick(event, item): void {
    if (item.disabled) {
      event.preventDefault();
      return;
    }

    // 点击菜单项时关闭 Dropdown
    if (this._dropdown.lvItemClickClose && !item.items) {
      this._dropdown.hoverItemId = null;
      this.menuService.mouseEnter$.next(false);
      this.menuService.open$.next(false);
      this._dropdown.dropopen$.next(false);
      this._dropdown.closeDropDown();
    }

    if (item.routerLink) {
      routerNavigate(this.router, item.routerLink);
    }

    this.click$.next({
      event,
      instance: this._dropdown,
      item,
    });
  }

  getOverlayConfig(target): OverlayConfig {
    return new OverlayConfig({
      hasBackdrop: false,
      positionStrategy: this.overlay
        .position()
        .flexibleConnectedTo(target)
        .withPositions([
          {
            originX: 'end',
            originY: 'top',
            overlayX: 'start',
            overlayY: 'top',
            offsetX: 0,
            offsetY: -1,
          },
          {
            originX: 'start',
            originY: 'top',
            overlayX: 'end',
            overlayY: 'top',
            offsetX: 0,
            offsetY: -1,
          },
        ]),
    });
  }

  ngOnInit(): void {
    this.menuService.menuOpen$.pipe(takeUntil(this.destroy$)).subscribe((data: boolean): void => {
      this.menuBaseService.menuOpen$.next(data);
    });

    combineLatest([this.menuService.open$])
      .pipe(auditTime(150), distinctUntilChanged(), takeUntil(this.destroy$))
      .subscribe(([open]): void => {
        if (!open) {
          if (this._overlay?.overlayRef.hasAttached()) {
            this._overlay.close();
          }
        }
      });

    merge(this.menuService.open$, this.menuService.mouseEnter$, this.openState$)
      .pipe(takeUntil(this.destroy$))
      .subscribe((): void => {
        this.cdr.markForCheck();
      });

    this.click$.pipe(take(1), takeUntil(this.destroy$)).subscribe(({ event, instance, item }): void => {
      if (item.onClick) {
        item.onClick({ event, instance, item });
      } else {
        this._dropdown.lvItemClick.emit({ event, instance, item });
      }
    });
  }

  openSubMenu(target, item): void {
    if (this._overlay?.overlayRef.hasAttached()) {
      this._overlay.close();
    }

    if (!item || !item.items || item.disabled) {
      return;
    }

    if (this._dropdown._isOpening || !this._dropdown._animationState.includes('show')) {
      const parentState = this._dropdown._animationState$.subscribe((open): void => {
        if (open) {
          parentState.unsubscribe();
          this.openSubMenu(target, item);
        }
      });
      return;
    }

    if (this._overlay?.overlayRef.hasAttached()) {
      this._overlay.overlayRef.dispose();
    }

    const config = this.getOverlayConfig(target);
    const extendConfig = this.getExtendConfig();

    const portal = new TemplatePortal(this.menuPanelTemplate, this.viewContainerRef, {
      $implicit: item,
      level: this.lvMenuLevel,
    });

    this._overlay = this.overlayService.open(portal, config, extendConfig);
    this._animationState = 'show';
  }

  getExtendConfig(): ExtendOverlayConfig {
    return {
      clickToCloseStrategy: this.lvItemClickClose ? undefined : { exclusions: '.lv-dropdown-item' },
      scrollToCloseStrategy: { exclusions: '.lv-dropdown-panel' },
      beforeClose: (overlayRef): Promise<void> =>
        new Promise<void>((resolve): void => {
          this.openState$.next(true);
          this._animationState = 'hide';

          // Add a mask after the animation started to stop mouse event
          overlayRef.overlayRef.overlayElement.style.position = 'relative';
          const div = document.createElement('div');
          div.setAttribute('style', 'background: rgba(0,0,0,0); left:0; top:0; bottom:0; right:0; position: absolute;');
          overlayRef.overlayRef.overlayElement.append(div);

          const closesubscribe = this.openState$.subscribe((open): void => {
            if (!open) {
              closesubscribe.unsubscribe();
              resolve();
            }
          });
        }),
    };
  }
}
